package de.mmis.core.base.abstracttree.serializer.internal;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Map;

import org.apache.log4j.Logger;

import de.mmis.core.base.DeserializableAnnotations.DeserializationMethod;
import de.mmis.core.base.MethodDescriptor;
import de.mmis.core.base.ObjectDescriptor;
import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.LeafNode.Encoding;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.abstracttree.serializer.Serializer;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.MethodInvocationException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.NoMatchingMethodFoundException;
import de.mmis.core.base.abstracttree.serializer.selector.InstanceSelector;
import de.mmis.core.base.abstracttree.serializer.selector.SimpleSelector;
import de.mmis.core.base.gclass.GClass;
import de.mmis.core.base.gclass.GParameterized;

/**
 * Default Serializer class for (de)serializing general java {@link Object}s
 * from/to Trees
 * 
 * @author Christian Koch
 */
public class ObjectSerializer implements Serializer<Object> {

	private final static Logger LOGGER = Logger
			.getLogger(ObjectSerializer.class);

	@DeserializationMethod
	public static InstanceSelector deserialize(Tree[] exps,
			ClassLoader classLoader, String id, Map<String, Object> idmap)
			throws DeserializableException {

		int nextindex = 1;

		Object object = null;

		if (exps.length == 0)
			throw new DeserializableException(new InnerNode(exps),
					"java-object Tree needs at least one parameter");

		Tree first = exps[0];

		GClass c = null;
		ArrayList<Tree> params = new ArrayList<Tree>();

		if (first.getType() == Tree.Type.LEAF
				&& ((LeafNode) first).getEncoding() == Encoding.Token
				&& ((LeafNode) first).getData().length != 0) {
			String className = ((LeafNode) first).getUTF8Data();
			try {
				c = GClass.fromString(className, classLoader);
			} catch (ClassNotFoundException e) {
				throw new DeserializableException(new InnerNode(exps), e,
						"cannot find class for given class name (" + className
								+ ")");
			}
		} else if (first.getType() == Tree.Type.INNER) {
			if (((InnerNode) first).getNumberOfSubTrees() == 0
					|| ((InnerNode) first).getSubTree(0).getType() != Tree.Type.LEAF)
				throw new DeserializableException(new InnerNode(exps),
						"object class is no valid atom or struct");
			String className = ((LeafNode) ((InnerNode) first).getSubTree(0))
					.getUTF8Data();
			try {
				c = GClass.fromString(className, classLoader);
			} catch (ClassNotFoundException e) {
				throw new DeserializableException(new InnerNode(exps), e,
						"cannot find class for given class name (" + className
								+ ")");
			}
			for (int i = 1; i < ((InnerNode) first).getNumberOfSubTrees(); i++)
				params.add(((InnerNode) first).getSubTree(i));
		} else
			throw new DeserializableException(new InnerNode(exps),
					"object class is no valid atom or struct");

		Class<?> baseClass = c.getBaseClass();
		if (baseClass == null)
			throw new DeserializableException(new InnerNode(exps),
					"cannot handle arrays here");

		Hashtable<TypeVariable<?>, GClass> typevarmap = new Hashtable<TypeVariable<?>, GClass>();
		if (c instanceof GParameterized)
			for (int i = 0; i < ((GParameterized) c).getTypeParameterClass().length; i++)
				typevarmap.put(baseClass.getTypeParameters()[i],
						((GParameterized) c).getTypeParameterClass()[i]);
		GClass.fromType(baseClass, typevarmap);

		boolean done = false;
		for (Constructor<?> cons : baseClass.getConstructors()) {
			if (Modifier.isPublic(cons.getModifiers())) {
				Type[] paraTypes = cons.getGenericParameterTypes();
				if (paraTypes.length == params.size()) {
					Object[] paraObjects = new Object[paraTypes.length];
					boolean ok = true;
					for (int i = 0; i < paraTypes.length; i++) {
						try {
							paraObjects[i] = SerializerHelper
									.deserializeObject(params.get(i),
											GClass.fromType(paraTypes[i],
													typevarmap), classLoader,
											idmap);
						} catch (DeserializableException e) {
							ok = false;
							break;
						}
					}
					if (ok) {
						try {
							object = cons.newInstance(paraObjects);
							done = true;
							break;
						} catch (IllegalArgumentException e) {
							throw new DeserializableException(
									new InnerNode(exps),
									e,
									"cannot create object instance with specified constructor (IllegalArgumentException)");
						} catch (InstantiationException e) {
							throw new DeserializableException(
									new InnerNode(exps),
									e,
									"cannot create object instance with specified constructor (InstantiationException)");
						} catch (IllegalAccessException e) {
							throw new DeserializableException(
									new InnerNode(exps),
									e,
									"cannot create object instance with specified constructor (IllegalAccessException)");
						} catch (InvocationTargetException e) {
							throw new DeserializableException(
									new InnerNode(exps),
									e,
									"cannot create object instance with specified constructor (InvocationTargetException)");
						}
					}
				}
			}
		}
		if (!done)
			throw new DeserializableException(new InnerNode(exps),
					"cannot find constructor matching the specified arguments");

		// instantiate as early as possible to add idmap entry in time in case
		// it is needed in further method calls
		InstanceSelector ret = new SimpleSelector(object, id, idmap);

		ObjectDescriptor od = new ObjectDescriptor(object, false);

		for (; nextindex < exps.length; nextindex++) {
			try {
				SerializerHelper.invokeObjectMethod(od, exps[nextindex],
						classLoader, idmap);
			} catch (MethodInvocationException e) {
				throw new DeserializableException(new InnerNode(exps), e,
						"error invoking initialization method "
								+ exps[nextindex]);
			} catch (NoMatchingMethodFoundException e) {
				throw new DeserializableException(new InnerNode(exps), e,
						"error invoking initialization method "
								+ exps[nextindex]);
			}
		}
		return ret;
	}

	public static InnerNode appendSerializedGetters(InnerNode s, Object object,
			boolean explicit, Map<Object, String> refmap,
			ClassLoader classLoader) {

		Hashtable<TypeVariable<?>, GClass> typevarmap = new Hashtable<TypeVariable<?>, GClass>();
		GClass.fromType(object.getClass(), typevarmap);

		ObjectDescriptor od = new ObjectDescriptor(object, false, typevarmap);

		for (MethodDescriptor md : od.getGetterMethodDescriptors()) {

			Object retval;
			try {
				retval = md.invoke();

				InnerNode temp = new InnerNode();
				temp.add(new LeafNode(("!" + md.getGetterName())));
				temp.add(Tree.fromObject(retval, explicit, refmap, classLoader));

				s.add(temp);
			} catch (IllegalAccessException e) {
				LOGGER.warn("error while invoking method " + md.getName()
						+ ", skipping", e);
			} catch (InvocationTargetException e) {
				LOGGER.warn("error while invoking method " + md.getName()
						+ ", skipping", e);
			}
		}
		return s;
	}

	@Override
	public Tree serialize(Object object, boolean explicit,
			Map<Object, String> refmap, ClassLoader classLoader) {

		InnerNode ret = new InnerNode();

		ret.add(new LeafNode(SerializerHelper.typeIdentifierPrefix
				+ "java-object"));
		ret.add(Tree.fromObject(object.getClass(), false, refmap, classLoader));
		appendSerializedGetters(ret, object, explicit, refmap, classLoader);
		return ret;
	}
}
